/**
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.

 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.

 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package org.jimm.protocols.icq.core;

import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import javax.net.SocketFactory;

import org.jimm.protocols.icq.tool.Dumper;
import android.util.Log;

/**
 * <p>
 * Created by
 * 
 * @author Fabrice Michellonet
 * @author Samolisov Pavel
 * @author Prolubnikov Dmitry
 * @author Konovalov Aleksey
 */
public class OscarClient {

	private static final String LOG_TAG = "ICQ:OscarClient";

	private static final int PACKET_HEADER_SIZE = 6;
	/** Время между попытками чтения из сокета (в мс) */
	private static final int SLEEP_TIME = 200;
	/** Время между проверками соединения (в мс) */
	private static final int PING_TIME = 10000;
	private static final int PING_TIMEOUT = 5000;

	private final String host;
	private final int port;

	private OscarPacketAnalyser analyser;
	private OscarPacketHandler handler;
	private final Receiver receiver = new Receiver();
	private Pinger pinger;

	private Socket socketClient;
	private InputStream in;
	private OutputStream out;

	private BlockingQueue<DataInputStream> messagesQueue = new LinkedBlockingQueue<DataInputStream>();
	private Listener listener;
	private volatile boolean isConnected = true;

	class Pinger extends Thread {

		private volatile boolean active = false;

		public Pinger() {
			super("Pinger thread");
			start();
		}

		public final void stopPinger() {
			active = false;
			pinger.interrupt();
		}

		@Override
		public void run() {
			if (active)
				return;

			active = true;
			setPriority(MIN_PRIORITY);
			try {
				while (active) {
					if (!socketClient.getInetAddress()
							.isReachable(PING_TIMEOUT) && active) {
						processConnectionError("Connection error");
						break;
					}
					Thread.sleep(PING_TIME);
				}
			} catch (InterruptedException e) {
				Log.d(LOG_TAG, "Pinger thread interrupted");
			} catch (IOException e) {
				processConnectionError(e.getLocalizedMessage());
			}
		}
	};

	class Receiver implements Runnable {

		private static final String THREAD_NAME = "OscarClientThread";
		private static final String LOG_TAG = OscarClient.LOG_TAG + ":Runner";

		private final byte header[] = new byte[PACKET_HEADER_SIZE];
		private Thread thread = null;

		public final void startReceiver() {
			thread = new Thread(this, THREAD_NAME);
			thread.start();
		}

		public final void stopReceiver() {
			if (thread != null) {
				thread.interrupt();
				thread = null;
			}
		}

		/**
		 * This function is ran by the Thread. It keeps reading the socket until
		 * 6 bytes at least are present. Those 6 bytes represents the
		 * <code>packet header</code>, from which we can determine the LENGTH of
		 * the packet. Then, the thread wait for <code>packetLen</code> bytes to
		 * be present on the socket, this is the <code>packet body</code>.
		 * Finaly, the <code>packet header</code> and the
		 * <code>packet body</code> are merged and sent to the packet analyser.
		 */
		@Override
		public void run() {

			byte packet[] = null;

			try {
				for(;;) {
					// Читаем заголовок
					readData(header, 0, PACKET_HEADER_SIZE);
					// retreiving flap datafield len
					int packetLen = getPacketLen(header);
					packet = new byte[packetLen + PACKET_HEADER_SIZE];
					readData(packet, PACKET_HEADER_SIZE, packetLen);
					System.arraycopy(header, 0, packet, 0, PACKET_HEADER_SIZE);
					messagesQueue.add(new DataInputStream(
							new ByteArrayInputStream(packet)));
					packet = null;
				}
			} catch (InterruptedException ex) {
				Log.e(LOG_TAG, ex.getMessage(), ex);
			} catch (InterruptedIOException ex) {
				Log.e(LOG_TAG, ex.getMessage(), ex);
			} catch (IOException ex) {
				Log.e(LOG_TAG, ex.getMessage(), ex);
				processConnectionError(ex.getLocalizedMessage());
			} catch (NullPointerException e) {
			}
		}
	}

	public interface Listener {
		/**
		 * Вызывается при возникновении ошибки соединения
		 * 
		 * @param errorDescription
		 *            описание ошибки
		 */
		void onConnectionError(String errorDescription);

		/**
		 * Вызывается при отключении клиента
		 */
		void onLogout();
	}

	/**
	 * This create a socket that will be connected to an Oscar Server.
	 * 
	 * @param host
	 *            The address of the server. <i>[i.e: <b>login.icq.com</b> for
	 *            ICQ]</i>
	 * @param port
	 *            The port on which the server is awaiting connection.
	 * @param analyser
	 *            The instance of the analyser that should be called to parse
	 *            the incoming packets.
	 */
	public OscarClient(String host, int port, OscarPacketAnalyser analyser,
			Listener listener) {
		this.analyser = analyser;
		this.host = host;
		this.port = port;
		this.listener = listener;
	}

	/**
	 * Переподключиться к другому серверу
	 * 
	 * @param host
	 * @param port
	 * @throws IOException
	 */
	public void reconnectTo(String host, int port) throws IOException {
		receiver.stopReceiver();
		closeSocket();
		messagesQueue.clear();
		openSocket(host, port);
		receiver.startReceiver();
	}

	private final void openSocket(String host, int port) throws UnknownHostException, IOException {
		socketClient = new Socket(host, port);

		out = new DataOutputStream(socketClient.getOutputStream());
		in = socketClient.getInputStream();
	}
	
	/**
	 * Make socket for connection to ICQ server and connect to it:
	 * <ul>
	 * <li>if <code>factory</code> is null then make socket with
	 * <code>new Socket(host, port)</code>
	 * <li>if <code>factory</code> is not null then make socket with
	 * {@link SocketFactory#createSocket(String, int)}
	 * </ul>
	 * 
	 * @throws IOException
	 * @throws UnknownHostException
	 */
	public void connect() throws UnknownHostException, IOException {
		if (isConnected)
			return;

		openSocket(host, port);

		pinger = new Pinger();
		receiver.startReceiver();

		messagesQueue.clear();
		analyser.initialize();
		handler = new OscarPacketHandler(analyser, messagesQueue);

		isConnected = true;
	}

	private final void closeSocket() {
		if (socketClient != null) {
			try {
				socketClient.shutdownInput();
				in = null;
			} catch (IOException e) {
				Log.e(LOG_TAG, "Disconnect error", e);
			}
			try {
				socketClient.shutdownOutput();
				out = null;
			} catch (IOException e) {
				Log.e(LOG_TAG, "Disconnect error", e);
			}
			try {
				socketClient.close();
				socketClient = null;
			} catch (IOException e) {
				Log.e(LOG_TAG, "Socket closing error", e);
			}
		}
	}

	/**
	 * Stoping the client
	 */
	public void disconnect() {
		if (pinger != null) {
			pinger.stopPinger();
			pinger = null;
		}

		receiver.stopReceiver();

		if (handler != null) {
			handler.stopHandler();
			handler = null;
		}

		closeSocket();

		isConnected = false;
	}

	// Прочитать в buffer данные
	private void readData(byte buffer[], int offset, int count)
			throws InterruptedException, IOException {
		for (;;) {
			int bytesReaded = in.read(buffer, offset, count);
			if (bytesReaded == -1)
				throw new InterruptedException();
			if (bytesReaded >= count)
				return;
			if (bytesReaded > 0) {
				offset += bytesReaded;
				count -= bytesReaded;
			}
			Thread.sleep(SLEEP_TIME);
		}
	}

	private void processConnectionError(String errorMsg) {
		isConnected = false;
		disconnect();
		if (listener != null)
			listener.onConnectionError(errorMsg);
	}

	/**
	 * Return the Inet address of the socket that is connected to the Oscar
	 * Server.
	 * 
	 * @return The Inet address of the the socket.
	 */
	public byte[] getInetaddress() {
		return socketClient.getLocalAddress().getAddress();
	}

	public BlockingQueue<DataInputStream> getMessageQueue() {
		return messagesQueue;
	}

	/**
	 * @param header
	 * @return
	 */
	private static int getPacketLen(byte[] header) {
		int result;
		result = (header[4] << 8) & 0xFF00;
		result |= header[5] & 0xFF;
		return result;
	}

	/**
	 * This function gives you the possibility to send packets throught the
	 * socket.
	 * 
	 * @param packet
	 *            The byte array representation of the packet to be sent.
	 */
	public void sendPacket(byte[] packet) {
		try {
			Dumper.log(packet, true, 8, 16);

			if (socketClient.isOutputShutdown() || socketClient.isClosed())
				throw new IOException();

			out.write(packet);
			out.flush();
		} catch (IOException e) {
			Log.e(LOG_TAG, "Send packet error", e);
			processConnectionError(e.getLocalizedMessage());
		} catch (NullPointerException e) {
			Log.d(LOG_TAG, "Oups :)");
		}
	}

	public synchronized void setListener(Listener l) {
		listener = l;
	}

	public final boolean isConnected() {
		return isConnected;
	}
}
